use anyhow::Result;
use bincode::{Decode, Encode};
use mime_guess::mime::TEXT_HTML_UTF_8;
use serde::{Deserialize, Serialize};
use turbo_rcstr::RcStr;
use turbo_tasks::{
    NonLocalValue, ReadRef, ResolvedVc, TaskInput, TryJoinIterExt, Vc, trace::TraceRawVcs,
};
use turbo_tasks_fs::{File, FileContent, FileSystemPath};
use turbo_tasks_hash::{Xxh3Hash64Hasher, encode_hex};
use turbopack_core::{
    asset::{Asset, AssetContent},
    chunk::{
        ChunkableModule, ChunkingContext, ChunkingContextExt, EvaluatableAssets,
        availability_info::AvailabilityInfo,
    },
    module::Module,
    module_graph::{ModuleGraph, chunk_group_info::ChunkGroup},
    output::{OutputAsset, OutputAssetsReference, OutputAssetsWithReferenced},
    version::{Version, VersionedContent},
};

#[derive(
    Clone,
    Debug,
    Deserialize,
    Eq,
    Hash,
    NonLocalValue,
    PartialEq,
    Serialize,
    TaskInput,
    TraceRawVcs,
    Encode,
    Decode,
)]
pub struct DevHtmlEntry {
    pub chunkable_module: ResolvedVc<Box<dyn ChunkableModule>>,
    pub module_graph: ResolvedVc<ModuleGraph>,
    pub chunking_context: ResolvedVc<Box<dyn ChunkingContext>>,
    pub runtime_entries: Option<ResolvedVc<EvaluatableAssets>>,
}

/// The HTML entry point of the dev server.
///
/// Generates an HTML page that includes the ES and CSS chunks.
#[turbo_tasks::value(shared)]
#[derive(Clone)]
pub struct DevHtmlAsset {
    path: FileSystemPath,
    entries: Vec<DevHtmlEntry>,
    body: Option<RcStr>,
}

#[turbo_tasks::value_impl]
impl OutputAssetsReference for DevHtmlAsset {
    #[turbo_tasks::function]
    fn references(self: Vc<Self>) -> Vc<OutputAssetsWithReferenced> {
        self.chunk_group()
    }
}

#[turbo_tasks::value_impl]
impl OutputAsset for DevHtmlAsset {
    #[turbo_tasks::function]
    fn path(&self) -> Vc<FileSystemPath> {
        self.path.clone().cell()
    }
}

#[turbo_tasks::value_impl]
impl Asset for DevHtmlAsset {
    #[turbo_tasks::function]
    fn content(self: Vc<Self>) -> Vc<AssetContent> {
        self.html_content().content()
    }

    #[turbo_tasks::function]
    fn versioned_content(self: Vc<Self>) -> Vc<Box<dyn VersionedContent>> {
        Vc::upcast(self.html_content())
    }
}

impl DevHtmlAsset {
    /// Create a new dev HTML asset.
    pub fn new(path: FileSystemPath, entries: Vec<DevHtmlEntry>) -> Vc<Self> {
        DevHtmlAsset {
            path,
            entries,
            body: None,
        }
        .cell()
    }

    /// Create a new dev HTML asset.
    pub fn new_with_body(
        path: FileSystemPath,
        entries: Vec<DevHtmlEntry>,
        body: RcStr,
    ) -> Vc<Self> {
        DevHtmlAsset {
            path,
            entries,
            body: Some(body),
        }
        .cell()
    }
}

#[turbo_tasks::value_impl]
impl DevHtmlAsset {
    #[turbo_tasks::function]
    pub async fn with_path(self: Vc<Self>, path: FileSystemPath) -> Result<Vc<Self>> {
        let mut html: DevHtmlAsset = self.owned().await?;
        html.path = path;
        Ok(html.cell())
    }

    #[turbo_tasks::function]
    pub async fn with_body(self: Vc<Self>, body: RcStr) -> Result<Vc<Self>> {
        let mut html: DevHtmlAsset = self.owned().await?;
        html.body = Some(body);
        Ok(html.cell())
    }
}

#[turbo_tasks::value_impl]
impl DevHtmlAsset {
    #[turbo_tasks::function]
    async fn html_content(self: Vc<Self>) -> Result<Vc<DevHtmlAssetContent>> {
        let this = self.await?;
        let context_path = this.path.parent();
        let mut chunk_paths = vec![];
        for chunk in &*self.chunk_group().await?.assets.await? {
            let chunk_path = &*chunk.path().await?;
            if let Some(relative_path) = context_path.get_path_to(chunk_path) {
                chunk_paths.push(format!("/{relative_path}").into());
            }
        }

        Ok(DevHtmlAssetContent::new(chunk_paths, this.body.clone()))
    }

    #[turbo_tasks::function]
    async fn chunk_group(&self) -> Result<Vc<OutputAssetsWithReferenced>> {
        let all_chunk_groups = self
            .entries
            .iter()
            .map(|entry| async move {
                let &DevHtmlEntry {
                    chunkable_module,
                    chunking_context,
                    module_graph,
                    runtime_entries,
                } = entry;

                let asset_with_referenced = if let Some(runtime_entries) = runtime_entries {
                    let runtime_entries =
                        if let Some(evaluatable) = ResolvedVc::try_downcast(chunkable_module) {
                            runtime_entries
                                .with_entry(*evaluatable)
                                .to_resolved()
                                .await?
                        } else {
                            runtime_entries
                        };
                    chunking_context
                        .evaluated_chunk_group_assets(
                            chunkable_module.ident(),
                            ChunkGroup::Entry(
                                runtime_entries
                                    .await?
                                    .iter()
                                    .map(|v| ResolvedVc::upcast(*v))
                                    .collect(),
                            ),
                            *module_graph,
                            AvailabilityInfo::root(),
                        )
                        .await?
                } else {
                    chunking_context
                        .root_chunk_group_assets(
                            chunkable_module.ident(),
                            ChunkGroup::Entry(vec![ResolvedVc::upcast(chunkable_module)]),
                            *module_graph,
                        )
                        .await?
                };

                Ok((
                    asset_with_referenced.assets.await?,
                    asset_with_referenced.referenced_assets.await?,
                    asset_with_referenced.references.await?,
                ))
            })
            .try_join()
            .await?;

        let mut all_assets = Vec::new();
        let mut all_referenced_assets = Vec::new();
        let mut all_references = Vec::new();
        for (asset, referenced_asset, reference) in all_chunk_groups {
            all_assets.extend(asset);
            all_referenced_assets.extend(referenced_asset);
            all_references.extend(reference);
        }

        Ok(OutputAssetsWithReferenced {
            assets: ResolvedVc::cell(all_assets),
            referenced_assets: ResolvedVc::cell(all_referenced_assets),
            references: ResolvedVc::cell(all_references),
        }
        .cell())
    }
}

#[turbo_tasks::value(operation)]
struct DevHtmlAssetContent {
    chunk_paths: Vec<RcStr>,
    body: Option<RcStr>,
}

impl DevHtmlAssetContent {
    fn new(chunk_paths: Vec<RcStr>, body: Option<RcStr>) -> Vc<Self> {
        DevHtmlAssetContent { chunk_paths, body }.cell()
    }
}

#[turbo_tasks::value_impl]
impl DevHtmlAssetContent {
    #[turbo_tasks::function]
    fn content(&self) -> Result<Vc<AssetContent>> {
        let mut scripts = Vec::new();
        let mut stylesheets = Vec::new();

        for relative_path in &*self.chunk_paths {
            if relative_path.ends_with(".js") {
                scripts.push(format!("<script src=\"{relative_path}\"></script>"));
            } else if relative_path.ends_with(".css") {
                stylesheets.push(format!(
                    "<link data-turbopack rel=\"stylesheet\" href=\"{relative_path}\">"
                ));
            } else {
                anyhow::bail!("chunk with unknown asset type: {}", relative_path)
            }
        }

        let body = match &self.body {
            Some(body) => body.as_str(),
            None => "",
        };

        let html: RcStr = format!(
            "<!DOCTYPE html>\n<html>\n<head>\n{}\n</head>\n<body>\n{}\n{}\n</body>\n</html>",
            stylesheets.join("\n"),
            body,
            scripts.join("\n"),
        )
        .into();

        Ok(AssetContent::file(
            FileContent::Content(File::from(html).with_content_type(TEXT_HTML_UTF_8)).cell(),
        ))
    }

    #[turbo_tasks::function]
    async fn version(self: Vc<Self>) -> Result<Vc<DevHtmlAssetVersion>> {
        let this = self.await?;
        Ok(DevHtmlAssetVersion { content: this }.cell())
    }
}

#[turbo_tasks::value_impl]
impl VersionedContent for DevHtmlAssetContent {
    #[turbo_tasks::function]
    fn content(self: Vc<Self>) -> Vc<AssetContent> {
        self.content()
    }

    #[turbo_tasks::function]
    fn version(self: Vc<Self>) -> Vc<Box<dyn Version>> {
        Vc::upcast(self.version())
    }
}

#[turbo_tasks::value(operation)]
struct DevHtmlAssetVersion {
    content: ReadRef<DevHtmlAssetContent>,
}

#[turbo_tasks::value_impl]
impl Version for DevHtmlAssetVersion {
    #[turbo_tasks::function]
    fn id(&self) -> Vc<RcStr> {
        let mut hasher = Xxh3Hash64Hasher::new();
        for relative_path in &*self.content.chunk_paths {
            hasher.write_ref(relative_path);
        }
        if let Some(body) = &self.content.body {
            hasher.write_ref(body);
        }
        let hash = hasher.finish();
        let hex_hash = encode_hex(hash);
        Vc::cell(hex_hash.into())
    }
}
